#!/usr/bin/env ruby
# This class can parse RedBook command strings and convert them into <tt>@command</tt> hashes.
class RedBook::Parser
	
	attr_accessor :command, :now

	@@special_tags = ['_break', '_concurrent']

	@@operations = [:log,:quit,:save,:config,:calc,:use,:refresh,
									:finish,:select,:ruby,:clear,:relog,:delete,:update,
									:dataset,:set]
	@@time_expressions = [:to,:from,:since,:until]
	@@flags = [:silent, :pending, :plain, :completed, :negate, :debug]
	@@attributes = [:tags, :dest, :type,:with, :message, :value]
	@@limits = [:last, :first]
	@@directives = []
	@@directives << @@operations
	@@directives << @@time_expressions
	@@directives << @@flags
	@@directives << @@attributes
	@@directives << @@limits
	@@directives.flatten!

	def self.macro?(sym) # :nodoc:
		@@config[:"m_#{sym.to_s}"]
	end


	# Dynamically define accessor methods for class variables
	class_variables.each do |v|
		name = v.gsub(/@/, '')
		(class << self; self; end).module_eval do
			define_method name.to_sym do
				module_eval v
			end
			unless name == "special_tags" then
				define_method "#{name.gsub(/s$/,'')}?".to_sym do |i|
					module_eval "#{v}.include? i"
				end
			end
			define_method "#{name}=".to_sym do |i|
				module_eval "#{v}.merge! i"
				@@directives.merge! i
			end
		end
	end
	
	# Redefine special_tag? method
	
	def self.special_tag?(tag)
		@@special_tags.each do |t|
			return true if tag = t || tag.match(/^#{t}/)
		end	
	end
	

	# Initialize config, plugins, etc.
	def initialize(now=Time.now)
		@now = now
		@hooks = RedBook::HookCollection.new
		RedBook::LOADED_PLUGINS.each do |p|
			send :"initialize_#{p}" if respond_to?(:"initialize_#{p}")
		end
		### HOOK
		@hooks.exec :before_initialize 
		@@config = RedBook::Config.instance.load
		@macro_value = nil
		@time_context = :past
		@regexps = {:directive => /(^:[a-z_]+){1}|(\s+:[a-z_]+){1}/, :negative_tag => /^[!]{1}.+/, :alias => /^([a-z_]+){1}$/, :escape => /\\:/}
		reset
		RedBook::PLUGINS.each do |p|
			send :"initialize_#{p}" if respond_to? :"initialize_#{p}"
		end
		### HOOK
		@hooks.exec :after_initialize 
	end

	# Reset <tt>@command</tt> and <tt>@macro_value</tt>
	def reset
		@command = {}
		@macro_value = nil
	end

	# Parse a RedBook command string, converting it to a <tt>@command</tt> hash.
	def parse(input)
		reset
		directives = split_directives(input.encode_log)
		i = 0
		while i < directives.length
			parse_directive(directives[i], directives[i+1])
			# Verify that the first directive is an operation
			if i == 0 then
				raise ParserError, "The first directive is not a valid operation." unless @command[:operation]
			end
			i = i+2
		end
		if @macro_value then
			interpret_macro	
		end
		@command
	end

	private

	def parse_directive(directive, argument="")
		if argument then
			argument.strip!
			argument.gsub!(@regexps[:escape], ":")
		end
		directive.gsub!(/^:/, '')
		directive.strip!
		directive = directive.to_sym
		if RedBook::Parser.operation?(directive) || RedBook::Parser.macro?(directive) then
			parse_operation(directive, argument)
		elsif RedBook::Parser.time_expression?(directive) then
			parse_time_expression(directive, argument)
		elsif  [:tags,:with].include?(directive) then
			parse_tags(argument)
		elsif RedBook::Parser.limit?(directive) then
			parse_limit(directive, argument)
		elsif RedBook::Parser.flag?(directive) then
			@command[directive] = true
		elsif directive == :message then
			@command[:text] = argument.gsub(@regexps[:escape], ":")
		elsif RedBook::Parser.attribute?(directive) then
			@command[:"#{directive}"] = argument
		else
			### HOOK
			unless @hooks.exec(:parse_directive, {:directive => directive, :argument => argument}) then
				raise ParserError, "Invalid directive \":#{directive.to_s}\"."
			end	
		end
	end

	def parse_operation(directive, argument)
		raise ParserError, "Attempt to reset command operation ':#{@command[:operation]}' to ':#{directive}'." if @command[:operation]
		@command[:operation] = directive
		argument = "" unless argument
		parse_macro(directive) if RedBook::Parser.macro?(directive)
		if (directive == :calc) || (directive == :delete)  then
			unless argument == "" then
				@command[:selected] = []
				ids = argument.split(/(\d+)/)
				ids.delete_if {|e| !e.match(/\d+/)}
				selected = []
				ids.each { |e| selected << e.to_i }
				@command[:selected] = selected
			else
				@command[:selected] = true
			end
		elsif [:use,:refresh].include? directive then
			if argument.match(@regexps[:alias]) || argument == '' then
				@command[:message] = argument
			else
				raise ParserError, "Illegal log alias."
			end
		elsif directive == :finish then
			if argument.match(/^\d+$/)||argument == '' then
				@command[:message] = argument
			else
				raise ParserError, "Illegal index value."
			end
		elsif directive == :update then
			if argument.match(/^\d+$/) then
				@command[:message] = argument
			elsif argument == "" then
				raise ParserError, "Index not specified."
			else
				raise ParserError, "Illegal index value."
			end
		else
			@command[:message] = argument unless argument == ""
		end
	end

	def parse_time_expression(directive, argument)
		result = nil
		begin
			result = Chronic.parse(argument, :context => @time_context, :now => @now)
		end
		if result.blank?
			begin
				# Try parsing with ActiveSupport
				result = instance_eval argument
			rescue
				raise ParserError, "Cannot parse time expression \"#{argument}\"."
			end
		end
		if [:from, :since].include? directive then
			@command[:from] = result
		else
			@command[:to] = result
		end
	end

	def parse_tags(argument)
		unless argument.blank? then
			@command[:tags] = []
			case @command[:operation]
			when :update then
				@command[:tags] = validate_update_tags strip_elements(argument.split(' '))
			when :select then
				alternatives = parse_tag_alternatives(argument)
				alternatives.each do |a|
					tags = validate_select_tags strip_elements(a.split(' '))
					positive, negative = define_tags(tags)
					@command[:tags] << {:positive => positive, :negative => negative}
				end
			else
				# :log and macros
				@command[:tags] = validate_log_tags strip_elements(argument.split(' ')) 
			end	
		end
	end

	def parse_limit(directive, argument)
		if argument.match(/\d+/) then
			if directive == :last then
				@command[:last] = argument.to_i
			elsif directive == :first then
				@command[:first] = argument.to_i
			end
		else
			raise ParserError, "Invalid limit \"#{argument}\"."
		end
	end

	def parse_type(argument)
		if @file_types.include? argument.downcase then
			@command[:type] = argument.downcase
		else
			raise ParserError, "Invalid file type \"#{argument}\"."
		end
	end

	def escape(string)
		string.gsub(/:/, '\:')
	end

	private

	def validate_log_tags(tags)
		validate_tags(tags, /^[!-+]/)
	end
	
	def validate_select_tags(tags)
		validate_tags(tags, /^[-+]/)
	end

	def validate_update_tags(tags)
		validate_tags(tags, /^!/)
	end
	
	def validate_tags(tags, regexp)
		tags.each do |t|
			raise ParserError, "Invalid tag: \"#{t}\"" if t.match(regexp) || (t.match(/^_/) && !RedBook::Parser.special_tag?(t) )
		end
		tags.uniq
	end

	def parse_macro(macro)
		macro = "m_#{macro.to_s}"
		RedBook::Config.instance.data.each_pair do |i,v|
			if i.to_s == macro then
				@macro_value = v.dup
				return
			end
		end
		raise ParserError, "Invalid macro: \"#{macro}\""
	end

	def interpret_macro
		# example ":log <:message> :with _twitter _to:<:to>"
		placeholders = @macro_value.scan(/<:([a-z_]+)>/).to_a.flatten
		placeholders.each do |p|
			subst = @command[p.to_sym] ? @command[p.to_sym] : ''
			@macro_value.gsub!(/<:#{p}>/, subst)
		end
		# Save tags & selected
		tags = @command[:tags] ? @command[:tags] : []
		selected = @command[:selected] ? @command[:selected] : []
		parse(@macro_value)
		if @command[:tags] then
			if @command[:operation] == :select then
				@command[:tags][:positive].merge! tags[:positive]
				@command[:tags][:negative].merge! tags[:negative]
			else
				@command[:tags] = tags.merge @command[:tags]
			end
		end
		@command[:selected].merge!(selected) if @command[:selected]
	end

	def define_tags(tags)
		positive = []
		negative = []
		tags.uniq.each do |t|
			if t.match(@regexps[:negative_tag]) then
				negative << t.gsub!('!', '')
			else
				positive << t
			end
		end
		return positive, negative
	end

	def parse_tag_alternatives(argument)
		strip_elements(argument.split('|'))
	end

	def strip_elements(array)
		array.each {|a| a.strip! }
	end

	def split_directives(string)
		directives = string.split(@regexps[:directive])
		directives.delete_at(0)
		directives.each { |d| d.strip! }
		directives
	end

end

